package com.example.demo.searcher;

import com.example.demo.model.DocInfo;
import com.example.demo.model.Result;
import com.example.demo.model.Weight;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.print.Doc;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: ling
 * Date: 2023-02-05
 * Time: 12:10
 */
@Service
public class DocSearcher {

    private Index index ;

    //暂停词存放路径
    private static final String STOP_WORD_PATH = "D:/doc_searcher_index/stop_word.txt";

    //使用这个hashset来保存暂停词
    private HashSet<String> stopWords = new HashSet<>();

    @Autowired
    public DocSearcher(Index index) {
        this.index = index;
        index.load();
        //这个方法用来加载暂停词到hashset当中
        loadStopWords();
    }

    /**
     * 通过这个方法来加载暂停词保存到hashset中
     */
    private void loadStopWords() {
        try(BufferedReader bufferedReader = new BufferedReader(new FileReader(STOP_WORD_PATH))) {
            while(true) {
                String line = bufferedReader.readLine();
                if(line == null) {
                    //读取完毕
                    break;
                }
                stopWords.add(line);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 这个方法会根据查询词, 进行搜索, 得到搜索结果集合.
     *  结果集合中包含若干条记录, 每个记录中包含搜索结果的标题, 描述, url
     * @param query
     * @return
     */
    public List<Result> search (String query) {
        //步骤1：分词：针对query这个查询词进行分词
        List<Term> oldTerms = ToAnalysis.parse(query).getTerms();
        List<Term> terms = new ArrayList<>();
        //步骤2：针对分词结果使用暂停词进行过滤
        for(Term term : oldTerms) {
            if(stopWords.contains(term.getName())) {
                continue;
            }
            terms.add(term);
        }
        //步骤3：触发：针对分词结果进行查倒排，
        List<List<Weight>> termResult = new ArrayList<>();
        for(Term term : terms) {
            String word = term.getName();
            List<Weight> invertedList = index.getInverted(word);
            if(invertedList==null) {
                //说明这个词在所有文档中不存在
                continue;
            }
            termResult.add(invertedList);
        }
        //步骤4：合并:针对多个分词结果触发的相同的文档进行合并
        List<Weight> allTermResult = mergeResult(termResult);
        //步骤5：排序：针对触发结果按权重排序
        allTermResult.sort(new Comparator<Weight>() {
            @Override
            public int compare(Weight o1, Weight o2) {
                //采用降序排序，权重高的在前面
                return o2.getWeight()- o1.getWeight();
            }
        });
        //步骤6：包装结果：针对排序结果去查正排，构造返回数据
        List<Result> results = new ArrayList<>();
        for (Weight weight : allTermResult) {
            DocInfo docInfo = index.getDocInfo(weight.getDocId());
            Result result = new Result();
            result.setTitle(docInfo.getTitle());
            result.setUrl(docInfo.getUrl());
            //通过getDesc方法获取摘要
            result.setDesc(getDesc(docInfo.getContent(),terms));
            results.add(result);
        }
        return results;
    }



    /**
     * 建一个内部类表示二维数组中的元素在几行几列
     */
    static class Pos {
        public int row;
        public int col;
        public Pos(int row,int col) {
            this.row = row;
            this.col = col;
        }
    }

    /**
     * 进行合并操作
     * 在进行合并的时候, 是把多个行合并成一行了.
     * 合并过程中势必是需要操作这个二维数组(二维List) 里面的每个元素
     *   操作元素就涉及到 "行" "列" 这样的概念~~ 要想确定二维数组中的一个元素, 就需要明确知道 行 和 列
     *   所以我们建一个内部类表示二维数组中的元素在几行几列
     * @param source
     * @return
     */
    private List<Weight> mergeResult(List<List<Weight>> source) {
        //1.先针对每一行进行排序（按文档id进行升序排序）
        for(List<Weight> curRow : source) {
            curRow.sort(new Comparator<Weight>() {
                @Override
                public int compare(Weight o1, Weight o2) {
                    return o1.getDocId()-o2.getDocId();
                }
            });
        }
        //2.借助优先级队列进行合并
        List<Weight> target = new ArrayList<>();
        PriorityQueue<Pos> queue = new PriorityQueue<>(new Comparator<Pos>() {
            @Override
            public int compare(Pos o1, Pos o2) {
                //先根据pos值找对应的weight对象，然后根据Weight的docId来排序
                Weight w1 = source.get(o1.row).get(o1.col);
                Weight w2 = source.get(o2.row).get(o2.col);
                return w1.getDocId()-w2.getDocId();
            }
        });
        //2.1 把每一行的每一个元素都放到优先级队列里
        for (int row = 0;row<source.size();row++) {
            queue.offer(new Pos(row,0));
        }
        //2.2循环取队首元素
        while (!queue.isEmpty()) {
            Pos minPos = queue.poll();
            Weight curWeight = source.get(minPos.row).get(minPos.col);
            //2.3看这个取的Weight的docId是否和前一个插入到target中的结果是一样的docId,如果是就合并
            if(target.size()>0) {
                //取出上次插入的元素
                Weight lastWeight = target.get(target.size()-1);
                if(lastWeight.getDocId()==curWeight.getDocId()) {
                    //合并权重
                    lastWeight.setWeight(lastWeight.getWeight()+curWeight.getWeight());
                } else {
                    target.add(curWeight);
                }
            } else {
                target.add(curWeight);
            }
            //2.4当前元素处理完毕后，当前元素所在的列表col++（光标往后移，处理下一个元素）
            Pos newPos = new Pos(minPos.row,minPos.col+1);
            if(newPos.col>=source.get(newPos.row).size()) {
                //如果当前列表到达最后一个元素
                continue;
            }
            queue.offer(newPos);

        }
        return target;
    }
    /**
     * 通过这个方法来获取摘要
     * @param content
     * @param terms
     * @return
     */
    private String getDesc(String content, List<Term> terms) {
        //1.先遍历content，看哪个关键词在content中出现
        int firstPos = -1;
        //分词会直接针对词进行转小写，所以要把正文转成小写
        content = content.toLowerCase();
        for(Term term : terms) {
            String word = term.getName();
            //此处采用“全写匹配”，让word独立成词，才要查找出来，而不是作为词的一部分
            //这里的正则表达式表示将  分隔符word分隔符替换成 空格word空格,只要查询就会找到整个单词而不是单词的一部分
            //比如输入array，但是搜索结果是arraylist，是它的一部分，所以要找到单独成词
            content = content.replaceAll("\\b" + word + "\\b"," " + word + " ");
            firstPos = content.indexOf(" " + word + " ");
            if(firstPos!=-1) {
                //找到关键词
                break;
            }
        }
        if(firstPos == -1) {
            //所有分词结果都不再正文中存在，这属于极端情况，直接返回一个空或正文前150个字符
            if(content.length()<150) {
                return content;
            }
            return content.substring(0,150) + "...";
        }
        //以firstPos作为基准位置，往前找50个字符，作为描述起始位置
        String desc = "";
        int descBeg = firstPos >50 ? firstPos-50 : 0;
        if(descBeg+150 > content.length()) {
            desc = content.substring(descBeg);
        } else {
            desc = content.substring(descBeg,descBeg+150);
        }
        //在此处加上一个替换操作，把描述中和分词结果相同的词加上一层<i>标签，可以通过replace实现（正则表达式）
        for(Term term : terms) {
            String word = term.getName();
            //注意此处要全字匹配,(?i) 表示所在位置右侧的表达式开启忽略大小写模式
            desc = desc.replaceAll("(?i) " + word +" ","<i> " + word + " </i>");
        }
        return desc;
    }
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        DocSearcher docSearcher = new DocSearcher(new Index());
        while (true) {
            System.out.println("->");
            String query =scanner.next();
            List<Result> results = docSearcher.search(query);
            for (Result result : results) {
                System.out.println("====================");
                System.out.println(result);
            }
        }
    }

}
